// Copyright (c) 2000 Just Objects B.V. <just@justobjects.nl>
// Distributable under LGPL license. See terms of license at gnu.org.

package nl.justobjects.pushlet.client;

import nl.justobjects.pushlet.core.Event;
import nl.justobjects.pushlet.core.EventParser;
import nl.justobjects.pushlet.core.Protocol;
import nl.justobjects.pushlet.core.Subscription;
import nl.justobjects.pushlet.util.PushletException;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Map;

/**
 * Client API for Java HTTP client applets or apps.
 * <p/>
 * Use this class within Java client applications or applets. Implement a
 * PushletClientListener to receive callbacks for data-related Event objects
 * pushed by the server.
 * <p/>
 * This class may also be used as a base class and be extended for custom
 * clients, hence the presence of many proteced methods.
 * 
 * @author Just van den Broecke - Just Objects &copy;
 * @version $Id: PushletClient.java,v 1.19 2009/06/04 12:46:35 justb Exp $
 * @see PushletClientListener
 * @see nl.justobjects.pushlet.test.PushletApplet
 * @see nl.justobjects.pushlet.test.PushletPingApplication
 */
public class PushletClient implements Protocol {
	/**
	 * Pushlet URL.
	 */
	private String pushletURL;

	/**
	 * Debug flag for verbose output.
	 */
	private boolean debug;
	
	private final String eventFormat = FORMAT_SERIALIZED_JAVA_OBJECT;
	private final String mode = MODE_STREAM; // 所有的client都以STREAM方式连接
	
	private String id;
	
	// pushclient状态
	private int state = STATE_NULL;
	public static final int STATE_ERROR = -2;
    public static final int STATE_ABORT = -1;
	public static final int STATE_NULL = 1;
    public static final int STATE_READY = 2;
	public static final int STATE_JOINED = 3;
    public static final int STATE_LISTENING = 4;
	public int getState() {
		return state;
	}

    public ArrayList<String[]> getSubjects() {
        return subjects;
    }
    public void setSubjects(ArrayList<String[]> subjects) {
        this.subjects = subjects;
    }
    private ArrayList<String[]> subjects = new ArrayList<String[]>();

	/**
	 * Internal listener for data events pushed by server.
	 */
	protected DataEventListener dataEventListener;

	/**
	 * Constructor with full pushlet URL.
	 */
	public PushletClient(String aPushletURL) {
		pushletURL = aPushletURL;
        state = STATE_READY;
	}

	/**
	 * Constructor with host and port using default URI.
	 */
	public PushletClient(String aHost, int aPort) {
		this("http://" + aHost + ":" + aPort + DEFAULT_SERVLET_URI);
	}
	
	public PushletClient(String aHost, int aPort, String path) {
		this("http://" + aHost + ":" + aPort + path + "/pushlet.srv");
	}

	/**
	 * Set proxy options and optional proxy authentication.
	 * <p/>
	 * Contributed by Dele Olajide See
	 * http://groups.yahoo.com/group/pushlet/message/634
	 * <p/>
	 * Usage: PushletClient pushletClient = new PushletClient("http:://www.domain.com/pushlet");
	 *        pushletClient.setProxyOptions("proxy.bla.com", "8080", ....);
	 * <p/>
	 * use pushletClient further as normal
	 */
	public void setProxyOptions(String aProxyHost, String aProxyPort, String theNonProxyHosts,
			String aUserName, String aPassword, String anNTLMDomain) {
		// Enable proxying
		System.setProperty("http.proxySet", "true");
		System.setProperty("http.proxyHost", aProxyHost);
		System.setProperty("http.proxyPort", aProxyPort);
		// Set optional non-proxy hosts
		if (theNonProxyHosts != null) {
			System.setProperty("http.nonProxyHosts", theNonProxyHosts);
		}
		// If user name specified configure proxy authentication
		if (aUserName != null) {
			System.setProperty("http.proxyUser", aUserName);
			System.setProperty("http.proxyPassword", aPassword);
			// See inner class below
			Authenticator.setDefault(new HTTPAuthenticateProxy(aUserName, aPassword));
			// Optional NT domain
			if (anNTLMDomain != null) {
				System.setProperty("http.auth.ntlm.domain", anNTLMDomain);
			}
		}
	}

	/**
	 * Join server, starts session.
	 */
	public void join() throws PushletException {
        if (state != STATE_READY)
            throw new PushletException("We can only join after initialization.");
		Event event = new Event(E_JOIN);
		event.setField(P_FORMAT, eventFormat);
		Event response = doControl(event);
		throwOnNack(response);
		// Join Ack received
		id = (String) response.getField(P_ID);
		state = STATE_JOINED;
	}
	
	/**
	 * 以指定id的方式链接服务器
	 * @param id
	 * @throws PushletException
	 */
	public void join(String id) throws PushletException {
        if (state != STATE_READY)
            throw new PushletException("We can only join after initialization.");
		Event event = new Event(E_JOIN);
		event.setField(P_FORMAT, eventFormat);
		event.setField(P_ID, id);
		Event response = doControl(event);
		throwOnNack(response);
		String pid = (String) response.getField(P_ID);
		if (!id.equals(pid))
			throw new PushletException("Algoritm to generate session key is not userid.");
		this.id = id;
        state = STATE_JOINED;
	}

	/**
	 * Leave server, stops session.
	 */
	public void leave() throws PushletException {
		stopListen();
		throwOnInvalidSession();
		Event event = new Event(E_LEAVE);
		event.setField(P_ID, id);
		Event response = doControl(event);
		throwOnNack(response);
		id = null;
		state = STATE_READY;
	}

	/**
	 * Open data channel.
	 */
	public void listen(PushletClientListener aListener) throws PushletException {
		listen(aListener, null, null);
	}

	/**
	 * Open data channel in stream or push mode with a subject.
	 */
	public void listen(PushletClientListener aListener, String aSubject, String label)
			throws PushletException {
		throwOnInvalidSession();
		stopListen();
		String listenURL = pushletURL + "?" + P_EVENT + "=" + E_LISTEN + "&" + P_ID + "=" + id
				+ "&" + P_MODE + "=" + mode;
		if (aSubject != null) {
			String[] subject = new String[2];
			subject[0] = aSubject;
			listenURL = listenURL + "&" + P_SUBJECT + "=" + aSubject;
			if (label != null) {
				listenURL = listenURL + "&" + P_LABEL + "=" + label;
				subject[1] = label;
			}
			subjects.add(subject);
		}
		// Start listener thread (sync call).
		startDataEventListener(aListener, listenURL);
	}

	/**
	 * Immediate listener: joins/subscribes and listens in one action.
	 */
	public void joinListen(PushletClientListener aListener, String aSubject, String label)
			throws PushletException {
		stopListen();
		String listenURL = pushletURL + "?" + P_EVENT + "=" + E_JOIN_LISTEN + "&" + P_FORMAT + "="
				+ eventFormat + "&" + P_MODE + "=" + mode;
		if (aSubject != null) {
			String[] subject = new String[2];
			subject[0] = aSubject;
			listenURL = listenURL + "&" + P_SUBJECT + "=" + aSubject;
			if (label != null) {
				listenURL = listenURL + "&" + P_LABEL + "=" + label;
				subject[1] = label;
			}
			subjects.add(subject);
		}
		// Start listener thread (sync call).
		startDataEventListener(aListener, listenURL);
	}
	
	public void joinListen(String id, PushletClientListener aListener, String aSubject, String label)
			throws PushletException {
		stopListen();
		String listenURL = pushletURL + "?" + P_EVENT + "=" + E_JOIN_LISTEN + "&" + P_FORMAT + "="
				+ eventFormat + "&" + P_MODE + "=" + mode + "&" + P_ID + "=" + id;
		if (aSubject != null) {
			String[] subject = new String[2];
			subject[0] = aSubject;
			listenURL = listenURL + "&" + P_SUBJECT + "=" + aSubject;
			if (label != null) {
				listenURL = listenURL + "&" + P_LABEL + "=" + label;
				subject[1] = label;
			}
			subjects.add(subject);
		}
		startDataEventListener(aListener, listenURL);
		this.id = id;
	}

    public void reconnect(String id, PushletClientListener aListener) throws PushletException {
        stopListen();
        String reconnectURL = pushletURL + "?" + P_EVENT + "=" + E_RECONNECT + "&" + P_FORMAT + "="
                + eventFormat + "&" + P_MODE + "=" + mode + "&" + P_ID + "=" + id;
        // 状态置为error，启动重连接
        state = STATE_ERROR;
        startDataEventListener(aListener, reconnectURL);
        this.id = id;
    }

	/**
	 * Publish an event through server.
	 */
	public void publish(String aSubject, String label, Map<String, Serializable> theAttributes) throws PushletException {
		throwOnInvalidSession();
		Event event = new Event(E_PUBLISH, theAttributes);
		event.setField(P_SUBJECT, aSubject);
		if (label != null)
			event.setField(P_LABEL, label);
		event.setField(P_ID, id);
		Event response = doControl(event);
		throwOnNack(response);
	}

	/**
	 * Subscribes, returning subscription id.
	 */
	public String subscribe(String aSubject, String aLabel) throws PushletException {
		throwOnInvalidSession();
		Event event = new Event(E_SUBSCRIBE);
		event.setField(P_ID, id);
		event.setField(P_SUBJECT, aSubject);
		String[] subject = new String[2];
		subject[0] = aSubject;
		// Optional label, is returned in data events
		if (aLabel != null) {
			event.setField(P_LABEL, aLabel);
			subject[1] = aLabel;
		}
		subjects.add(subject);
		// Send request
		Event response = doControl(event);
		throwOnNack(response);
		// return (String) response.getField(P_SUBSCRIPTION_ID);
        return Subscription.compositeSubject(aSubject, aLabel);
	}

	/**
	 * Subscribes, returning subscription id.
	 */
	public String subscribe(String aSubject) throws PushletException {
		return subscribe(aSubject, null);
	}

	/**
	 * Unsubscribes with subscription id.
	 */
	/*public void unsubscribe(String aSubscriptionId) throws PushletException {
		throwOnInvalidSession();
		Event event = new Event(E_UNSUBSCRIBE);
		event.setField(P_ID, id);

		// Optional subscription id
		if (aSubscriptionId != null) {
			event.setField(P_SUBSCRIPTION_ID, aSubscriptionId);
		}

		Event response = doControl(event);
		throwOnNack(response);
		removeSuject(response.getSubject(), (String) response.getField(P_LABEL));
	}*/
	
	private void removeSuject(String aSubject, String aLabel) {
		if (aSubject == null) {
			subjects.clear();
			return;
		}
		int idx = -1;
		for (String[] subjectArr : subjects) {
			String subject = subjectArr[0];
			String label = subjectArr[1];
			++idx;
			if ((label == null && aLabel != null) || (label != null && aLabel == null))
				continue;
			else if (label != null && !label.equals(aLabel))
				continue;
			else if (!aSubject.equals(subject))
				continue;
			subjects.remove(idx);
			break;
		}
	}
	
	/**
	 * 以指定subject和label的方式unsubscribe
	 * @param aSubject
	 * @param label
	 * @throws PushletException
	 */
	public void unsubscribe(String aSubject, String label) throws PushletException {
		throwOnInvalidSession();
		Event event = new Event(E_UNSUBSCRIBE);
		event.setField(P_ID, id);
        if (aSubject != null)
		    event.setField(P_SUBJECT, aSubject);
		if (label != null)
			event.setField(P_LABEL, label);
		Event response = doControl(event);
		throwOnNack(response);
		removeSuject(aSubject, label);
	}

	/**
	 * Unsubscribes from all subjects.
	 */
	public void unsubscribe() throws PushletException {
		unsubscribe(null, null);
	}

	/**
	 * Stop the listener.
	 */
	public void stopListen() throws PushletException {
		if (dataEventListener != null) {
			unsubscribe();
			dataEventListener.stop();
			dataEventListener = null;
		}
	}

	public void setDebug(boolean b) {
		debug = b;
	}

	/**
	 * Starts default DataEventListener and waits for its thread to start.
	 * @throws PushletException 
	 */
	protected void startDataEventListener(PushletClientListener aListener, 
			String aListenURL) throws PushletException {
		if (dataEventListener != null)
			throw new PushletException("PushletsClient already listenning...");
		// Suggestion by Jeff Nowakowski 29.oct.2006
		dataEventListener = new DataEventListener(aListener, aListenURL);
		synchronized (dataEventListener) {
			dataEventListener.start();
			try {
				// Wait for data event listener (thread) to start
				dataEventListener.wait();
			} catch (InterruptedException e) {
			}
		}
	}

	protected void throwOnNack(Event anEvent) throws PushletException {
		if (anEvent.getEventType().equals(E_NACK)) {
			throw new PushletException("Negative response: reason=" + anEvent.getField(P_REASON));
		}
	}

	protected void throwOnInvalidSession() throws PushletException {
		if (id == null) {
			throw new PushletException("Invalid pushlet session");
		}
	}

	protected BufferedInputStream openURL(String aURL) throws PushletException {
		// Open URL connection with server
		try {
			p("Connecting to " + aURL);
			URL url = new URL(aURL);
			URLConnection urlConnection = url.openConnection();

			// Disable any kind of caching.
			urlConnection.setUseCaches(false);
			urlConnection.setDefaultUseCaches(false);

            // 超时设置，防止网络异常的情况下，程序僵死而不继续往下执行
            urlConnection.setConnectTimeout(88000);
            urlConnection.setReadTimeout(30000);

            if (state == STATE_ERROR) {
                // 重连接
                StringBuilder cookieValue = new StringBuilder(Protocol.COOKIE_NAME).append("=");
                for (int i = 0, size = subjects.size(); i < size; i++) {
                    String[] subjectAttr = subjects.get(i);
                    String subjct = subjectAttr[0];
                    String label = subjectAttr[1];
                    cookieValue.append(subjct);
                    if (label != null)
                        cookieValue.append(Protocol.SUBJECT_LABEL_GAP).append(label);
                    if (i != (size - 1))
                        cookieValue.append(Protocol.SUBJECTS_GAP);
                }
                urlConnection.setRequestProperty("Cookie", cookieValue.toString());
            }

			// Get the stream from the server.
			// reader = new BufferedReader(new
			// InputStreamReader(urlConnection.getInputStream()));
			// Note: somehow the client does not work with some JVMs when using
			// BufferedInputStream... So do unbuffered input.
			// p("Opening urlConnection inputstream");
			return new BufferedInputStream(urlConnection.getInputStream());

		} catch (Throwable t) {
            state = STATE_ERROR;
			warn("openURL() could not open " + aURL, t);
			throw new PushletException(" could not open " + aURL, t);
		}
	}

    protected BufferedInputStream openURL(Event event) throws PushletException {
        try {
            p("Connecting to " + pushletURL);
            URL url = new URL(pushletURL);
            URLConnection urlConnection = url.openConnection();

            urlConnection.setUseCaches(false);
            urlConnection.setDefaultUseCaches(false);

            urlConnection.setConnectTimeout(88000);
            urlConnection.setReadTimeout(30000);

            urlConnection.setDoOutput(true);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(urlConnection.getOutputStream());
            objectOutputStream.writeObject(event);
            objectOutputStream.flush();
            objectOutputStream.close();

            return new BufferedInputStream(urlConnection.getInputStream());
        } catch (Throwable t) {
            state = STATE_ERROR;
            warn("openURL() could not open " + pushletURL, t);
            throw new PushletException(" could not open " + pushletURL, t);
        }
    }

	/**
	 * Send control events to server and return response.
	 */
	protected Event doControl(Event aControlEvent) throws PushletException {
		// String controlURL = pushletURL + "?" + aControlEvent.toQueryString();

		p("doControl to ", aControlEvent);

		// Open URL connection with server
		BufferedInputStream in = openURL(aControlEvent);

		// Get Pushlet event from stream
		Event event;
		try {
			p("Getting event...");
			// Get next event from server
			event = EventParser.parse(in);
			p("Event received ", event);
			return event;
		} catch (Throwable t) {
			// Stop and report error.
			warn("doControl() exception", t);
			throw new PushletException(" error parsing response from " + pushletURL, t);
		} finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

	/**
	 * Util: print.
	 */
	protected void p(String s) {
		if (debug) {
			System.out.println("[PushletClient] " + s);
		}
	}

    protected void p(String s, Event event) {
        if (debug) {
            System.out.println("[PushletClient] " + s + event);
        }
    }

	/**
	 * Util: warn.
	 */
	protected void warn(String s) {
		warn(s, null);
	}

	/**
	 * Util: warn with exception.
	 */
	protected void warn(String s, Throwable t) {
		System.err.println("[PushletClient] - WARN - " + s + " ex=" + t);

		if (t != null) {
			t.printStackTrace();
		}
	}

	/**
	 * Internal (default) listener for the Pushlet data channel.
	 */
	protected class DataEventListener implements Runnable {
		/**
		 * Client's listener that gets called back on events.
		 */
		private PushletClientListener listener;

		/**
		 * Receiver receiveThread.
		 */
		private Thread receiveThread = null;
		private BufferedInputStream in;
		private String refreshURL;
		private String listenURL;

		public DataEventListener(PushletClientListener aListener, String aListenURL) {
			listener = aListener;
			listenURL = aListenURL;
		}

		public void start() {
			// All ok: start a receiver receiveThread
			receiveThread = new Thread(this);
			receiveThread.start();
		}

		/**
		 * Stop listening; may restart later with start().
		 */
		public void stop() {
			p("In stop()");
			bailout();
		}

		/**
		 * Receive event objects from server and callback listener.
		 */
		public void run() {
			p("Start run()");
			try {
				while (receiveThread != null && receiveThread.isAlive()) {
					// Connect to server
					in = openURL(listenURL);
					state = STATE_LISTENING;
					synchronized (this) {
						// Inform the calling thread we're ready to receive
						// events.
						// Suggestion by Jeff Nowakowski 29.oct.2006
						this.notify();
					}

					// Get events while we're alive.
					while (receiveThread != null && receiveThread.isAlive()) {
						Event event;
						try {
							// p("Getting event...");
							// Get next event from server
							event = EventParser.parse(in);
							p("Event received ", event);
						} catch (Throwable t) {
							// Stop and report error.
							// warn("Stop run() on exception", t);
							if (receiveThread != null && listener != null) {
								listener.onError("exception during receive: " + t);
								state = STATE_ERROR;
							}
							// t.printStackTrace();
							break;
						}

						// Handle event by calling listener
						if (event != null && listener != null) {
							// p("received: " + event.toXML());
							String eventType = event.getEventType();
							if (eventType.equals(E_HEARTBEAT)) {
								listener.onHeartbeat(event);
							} else if (eventType.equals(E_DATA)) {
								listener.onData(event);
							} else if (eventType.equals(E_JOIN_LISTEN_ACK)) {
								id = (String) event.getField(P_ID);
							} else if (eventType.equals(E_LISTEN_ACK)) {
								p("Listen ack ok");
							} else if (eventType.equals(E_REFRESH_ACK)) {
								// ignore
							} else if (eventType.equals(E_ABORT)) {
								p("Server abort");
								listener.onAbort(event);
								listener = null;
								state = STATE_ABORT;
								break;
							} else if (eventType.equals(E_REFRESH)) {
								refresh(event);
							} else if (eventType.equals(E_RECONNECT_ACK)) {
								state = STATE_LISTENING;
							} else {
								handleUnknownEventType(eventType, event, listener);
							}
						}
					}
				}
			} catch (Throwable t) {
				warn("Exception in run() ", t);
				// bailout();
				state = STATE_ERROR;
				synchronized (this) {
					// 唤醒退出
					this.notify();
				}
			}
        }

		protected void disconnect() {
			p("start disconnect()");
			if (in != null) {
				try {
					// this blocks, find another way
                    in.close();
					p("Closed inputstream ok");
				} catch (Exception ignore) {
				} finally {
					in = null;
				}
			}
			p("end disconnect()");
		}

		/**
		 * Stop receiver receiveThread.
		 */
		public void stopThread() {
			p("In stopThread()");
			// Keep a reference such that we can kill it from here.
			Thread targetThread = receiveThread;
			receiveThread = null;
			// This should stop the main loop for this receiveThread.
			// Killing a receiveThread on a blcing read is tricky.
			// See also http://gee.cs.oswego.edu/dl/cpj/cancel.html
			if ((targetThread != null) && targetThread.isAlive()) {
				targetThread.interrupt();
				try {
					// Wait for it to die
					targetThread.join(500);
				} catch (InterruptedException ignore) {
				}
				// If current receiveThread refuses to die,
				// take more rigorous methods.
				/*if (targetThread.isAlive()) {
					// Not preferred but may be needed
					// to stop during a blocking read.
					targetThread.stop();
					// Wait for it to die
					try {
						targetThread.join(500);
					} catch (Throwable ignore) {
					}
				}*/
				p("Stopped receiveThread alive=" + targetThread.isAlive());
			}
		}

		/**
		 * Stop listening on stream from server.
		 */
		public void bailout() {
			p("In bailout()");
			stopThread();
			disconnect();
		}

		/**
		 * Handle refresh, by pausing.
		 */
		protected void refresh(Event aRefreshEvent) throws PushletException {
			try {
				// Wait for specified time.
				Thread.sleep(((Long) aRefreshEvent.getField(P_WAIT)).longValue());
			} catch (Throwable t) {
				warn("abort while refresing");
				refreshURL = null;
				return;
			}
			// If stopped during sleep, don't proceed
			if (receiveThread == null) {
				return;
			}
			// Create url to refresh
			refreshURL = pushletURL + "?" + P_ID + "=" + id + "&" + P_EVENT + "=" + E_REFRESH;
			if (in != null) {
				try {
					in.close();
				} catch (IOException ignore) {
				}
				in = null;
			}
			in = openURL(refreshURL);
		}

		/**
		 * Handle unknown Event (default behaviour).
		 */
		protected void handleUnknownEventType(String eventType, Event event,
				PushletClientListener listener) {
			warn("unsupported event type received: " + eventType);
		}
	}

	/**
	 * Authenticator
	 */
	private static class HTTPAuthenticateProxy extends Authenticator {
		/**
		 * Contributed by Dele Olajide See
		 * http://groups.yahoo.com/group/pushlet/message/634
		 */
		private String thePassword = "";
		private String theUser = "";

		public HTTPAuthenticateProxy(String username, String password) {
			thePassword = password;
			theUser = username;
		}

		protected PasswordAuthentication getPasswordAuthentication() {
			// System.out.println("[HttpAuthenticateProxy] Username = " +
			// theUser);
			// System.out.println("[HttpAuthenticateProxy] Password = " +
			// thePassword);
			return new PasswordAuthentication(theUser, thePassword.toCharArray());
		}
	}

}

/*
 * $Log: PushletClient.java,v $ Revision 1.19 2009/06/04 12:46:35 justb
 * PushletClient: add more hooks for extension (feat ID: 2799694 Craig M)
 * 
 * Revision 1.18 2007/11/10 13:52:47 justb make startDataEventListener method
 * protected to allow overriding
 * 
 * Revision 1.17 2006/10/29 16:47:57 justb included patch from Jeff Nowakowski:
 * wait until listener thread runs
 * 
 * Revision 1.16 2005/05/06 20:08:20 justb client enhancements
 * 
 * Revision 1.15 2005/03/27 17:42:27 justb enhancements
 * 
 * Revision 1.14 2005/03/25 23:54:04 justb *** empty log message ***
 * 
 * Revision 1.13 2005/02/28 16:59:40 justb fixes for leave and disconnect
 * 
 * Revision 1.12 2005/02/28 15:57:54 justb added SimpleListener example
 * 
 * Revision 1.11 2005/02/21 12:31:44 justb added proxy contribution from Dele
 * Olajide
 * 
 * Revision 1.10 2005/02/20 13:05:32 justb removed the Postlet (integrated in
 * Pushlet protocol)
 * 
 * Revision 1.9 2005/02/18 10:07:23 justb many renamings of classes (make names
 * compact)
 * 
 * Revision 1.8 2005/02/18 09:54:12 justb refactor: rename Publisher Dispatcher
 * and single Subscriber class
 * 
 * Revision 1.7 2005/02/15 15:46:30 justb client API improves
 * 
 * Revision 1.6 2005/02/15 13:28:56 justb first quick rewrite adapt for v2
 * protocol
 * 
 * Revision 1.5 2004/10/25 21:23:44 justb *** empty log message ***
 * 
 * Revision 1.4 2004/10/24 13:52:51 justb small fixes in client lib
 * 
 * Revision 1.3 2004/10/24 12:58:18 justb revised client and test classes for
 * new protocol
 * 
 * Revision 1.2 2004/09/03 22:35:37 justb Almost complete rewrite, just checking
 * in now
 * 
 * Revision 1.1 2004/03/10 20:14:17 justb renamed all *JavaPushletClient* to
 * *PushletClient*
 * 
 * Revision 1.10 2004/03/10 15:45:55 justb many cosmetic changes
 * 
 * Revision 1.9 2003/08/17 20:30:20 justb cosmetic changes
 * 
 * Revision 1.8 2003/08/15 08:37:40 justb fix/add Copyright+LGPL file headers
 * and footers
 */